/* *****************************************************************************
 * JFab (http://code.google.com/p/jfab)
 * Copyright (c) 2011 JFab.org
 * Admin jfab@jeffreythompson.net
 *
 * See the file "LICENSE.txt" for information on usage and redistribution of
 * this file, and for a DISCLAIMER OF ALL WARRANTIES.
 * *****************************************************************************/

package org.jfab.core.factory;

import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.Validate;
import org.javaruntype.type.StandardTypeParameter;
import org.javaruntype.type.Type;
import org.javaruntype.type.TypeParameter;
import org.javaruntype.type.Types;

/**
 * Provides an instance factory.
 */
public class DefaultInstanceFactory implements InstanceFactory
{
    /** Default array length. */
    private static final Integer DEFAULT_ARRAY_LENGTH = 5;

    /**
     * Factory method.
     * 
     * @return a new instance factory.
     */
    public static DefaultInstanceFactory create()
    {
        return new DefaultInstanceFactory();
    }

    /** Boolean value. */
    private Boolean _booleanValue = true;

    /** Byte value. */
    private Byte _byteValue = 0;

    /** Character value. */
    private Character _characterValue = 'a';

    /** Double value. */
    private Double _doubleValue = 1.0;

    /** Float value. */
    private Float _floatValue = Float.valueOf("1.0");

    /** Integer value. */
    private Integer _integerValue = 1;

    /** Long value. */
    private Long _longValue = 1000L;

    /** Short value. */
    private Short _shortValue = 1;

    /** String counter. */
    private Integer _stringCounter = 1;

    @Override
    public <T> T createInstance(final Type<T> type)
    {
        Validate.notNull(type, "type is null");

        T answer = null;

        if (type.isArray())
        {
            @SuppressWarnings("unchecked")
            final Class<T> componentClass = (Class<T>)type.getComponentClass();
            final Type<?> componentType = Types.forClass(componentClass);
            @SuppressWarnings("unchecked")
            final T value = (T)createArray(componentType, DEFAULT_ARRAY_LENGTH);
            answer = value;
        }
        else
        {
            final Constructor<T> constructor = getSimplestConstructor(type);

            if (constructor == null)
            {
                @SuppressWarnings("unchecked")
                final T value = createInstanceNoConstructor((Class<T>)type
                        .getRawClass());
                answer = value;
            }
            else
            {
                answer = createInstanceFromConstructor(constructor);
            }
        }

        return answer;
    }

    @Override
    public <T> T createInstancePopulated(final Type<T> type)
    {
        Validate.notNull(type, "type is null");

        T answer = null;

        try
        {
            if (type.getRawClass() == String.class)
            {
                @SuppressWarnings("unchecked")
                final T value = (T)createString();
                answer = value;
            }
            else if (type.isArray())
            {
                @SuppressWarnings("unchecked")
                final Class<T> componentClass = (Class<T>)type
                        .getComponentClass();
                final Type<?> componentType = Types.forClass(componentClass);

                @SuppressWarnings("unchecked")
                final T value = (T)createArrayPopulated(componentType,
                        DEFAULT_ARRAY_LENGTH);
                answer = value;
            }
            else
            {
                answer = createInstance(type);

                if (answer != null)
                {
                    if (answer instanceof Collection)
                    {
                        final Type<?> subtype = getType(type
                                .getTypeParameters().get(0));

                        if (subtype != null)
                        {
                            for (int i = 0; i < DEFAULT_ARRAY_LENGTH; i++)
                            {
                                final Object value = createInstancePopulated(subtype);
                                ((Collection)answer).add(value);
                            }
                        }
                    }
                    else if (answer instanceof Map)
                    {
                        final Type<?> subtype0 = getType(type
                                .getTypeParameters().get(0));
                        final Type<?> subtype1 = getType(type
                                .getTypeParameters().get(1));

                        if ((subtype0 != null) && (subtype1 != null))
                        {
                            for (int i = 0; i < DEFAULT_ARRAY_LENGTH; i++)
                            {
                                final Object key = createInstancePopulated(subtype0);
                                final Object value = createInstancePopulated(subtype1);
                                ((Map)answer).put(key, value);
                            }
                        }
                    }
                    else
                    {
                        final ClassAnalyzer analyzer = ClassAnalyzer
                                .getInstance(type.getRawClass());
                        final Collection<Method> mutators = analyzer
                                .getMutatorMethods();

                        if (CollectionUtils.isNotEmpty(mutators))
                        {
                            for (final Method mutator : analyzer
                                    .getMutatorMethods())
                            {
                                final Type<?> parameterType = Types
                                        .forClass(mutator.getParameterTypes()[0]);
                                final Object arg = createInstancePopulated(parameterType);

                                if (arg != null)
                                {
                                    mutator.invoke(answer, arg);
                                }
                            }
                        }
                    }
                }
            }
        }
        catch (final IllegalArgumentException e)
        {
            throw new InstanceCreationException(e);
        }
        catch (final IllegalAccessException e)
        {
            throw new InstanceCreationException(e);
        }
        catch (final InvocationTargetException e)
        {
            throw new InstanceCreationException(e);
        }

        return answer;
    }

    /**
     * @param <T> Type.
     * @param componentType Component type.
     * @param length Length.
     * 
     * @return a new array.
     */
    private <T> T[] createArray(final Type<T> componentType,
            final Integer length)
    {
        T[] answer = null;

        @SuppressWarnings("unchecked")
        final T[] array = (T[])Array.newInstance(componentType.getRawClass(),
                length);
        answer = array;

        return answer;
    }

    /**
     * @param <E> Element type.
     * 
     * @return a new array list.
     */
    private <E> ArrayList<E> createArrayList()
    {
        return new ArrayList<E>();
    }

    /**
     * @param <T> Type.
     * @param componentType Component type.
     * @param length Length.
     * 
     * @return a new populated array.
     * 
     * @throws InstanceCreationException if there is any problem.
     */
    private <T> T[] createArrayPopulated(final Type<T> componentType,
            final Integer length)
    {
        final T[] answer = createArray(componentType, length);

        for (int i = 0; i < length; i++)
        {
            final T value = createInstancePopulated(componentType);
            answer[i] = value;
        }

        return answer;
    }

    /**
     * @return a new boolean.
     */
    private synchronized Boolean createBoolean()
    {
        final Boolean answer = _booleanValue;
        _booleanValue = !_booleanValue;

        return answer;
    }

    /**
     * @return a new byte.
     */
    private synchronized Byte createByte()
    {
        final Byte answer = _byteValue;
        _byteValue = (byte)(_byteValue + 1);

        return answer;
    }

    /**
     * @return a new character.
     */
    private synchronized Character createCharacter()
    {
        final Character answer = _characterValue;
        _characterValue = (char)(_characterValue.charValue() + 1);

        return answer;
    }

    /**
     * @return a new double.
     */
    private synchronized Double createDouble()
    {
        final Double answer = _doubleValue;
        _doubleValue += 0.1;

        return answer;
    }

    /**
     * @return a new float.
     */
    private synchronized Float createFloat()
    {
        final Float answer = _floatValue;
        _floatValue = (float)(_floatValue + 0.1);

        return answer;
    }

    /**
     * @param <K> Key type.
     * @param <V> Value type.
     * 
     * @return a new hash map.
     */
    private <K, V> HashMap<K, V> createHashMap()
    {
        return new HashMap<K, V>();
    }

    /**
     * @param <E> Element type.
     * 
     * @return a new hash set.
     */
    private <E> HashSet<E> createHashSet()
    {
        return new HashSet<E>();
    }

    /**
     * @param <T> Type.
     * @param constructor Constructor.
     * 
     * @return a new instance from the given constructor.
     * 
     * @throws InstanceCreationException if there is any problem.
     */
    private <T> T createInstanceFromConstructor(final Constructor<T> constructor)
    {
        T answer = null;

        final Class<?>[] parameterClasses = constructor.getParameterTypes();
        final List<Object> objectList = new ArrayList<Object>();

        for (final Class<?> parameterClass : parameterClasses)
        {
            final Type<?> parameterType = Types.forClass(parameterClass);
            final Object arg = createInstancePopulated(parameterType);
            objectList.add(arg);
        }

        final Object[] initargs = objectList.toArray();

        try
        {
            answer = constructor.newInstance(initargs);
        }
        catch (final IllegalArgumentException e)
        {
            throw new InstanceCreationException(e);
        }
        catch (final IllegalAccessException e)
        {
            throw new InstanceCreationException(e);
        }
        catch (final InvocationTargetException e)
        {
            throw new InstanceCreationException(e);
        }
        catch (final InstantiationException e)
        {
            throw new InstanceCreationException(e);
        }

        return answer;
    }

    /**
     * @param <T> Type.
     * @param aClass Class.
     * 
     * @return a new instance of the given class.
     */
    private <T> T createInstanceNoConstructor(final Class<T> aClass)
    {
        T answer = null;

        if (boolean.class.equals(aClass))
        {
            @SuppressWarnings("unchecked")
            final T value = (T)createBoolean();
            answer = value;
        }
        else if (byte.class.equals(aClass))
        {
            @SuppressWarnings("unchecked")
            final T value = (T)createByte();
            answer = value;
        }
        else if (char.class.equals(aClass))
        {
            @SuppressWarnings("unchecked")
            final T value = (T)createCharacter();
            answer = value;
        }
        else if (double.class.equals(aClass))
        {
            @SuppressWarnings("unchecked")
            final T value = (T)createDouble();
            answer = value;
        }
        else if (float.class.equals(aClass))
        {
            @SuppressWarnings("unchecked")
            final T value = (T)createFloat();
            answer = value;
        }
        else if (int.class.equals(aClass))
        {
            @SuppressWarnings("unchecked")
            final T value = (T)createInteger();
            answer = value;
        }
        else if (long.class.equals(aClass))
        {
            @SuppressWarnings("unchecked")
            final T value = (T)createLong();
            answer = value;
        }
        else if (short.class.equals(aClass))
        {
            @SuppressWarnings("unchecked")
            final T value = (T)createShort();
            answer = value;
        }
        else if (List.class.equals(aClass))
        {
            @SuppressWarnings("unchecked")
            final T value = (T)createArrayList();
            answer = value;
        }
        else if (Map.class.equals(aClass))
        {
            @SuppressWarnings("unchecked")
            final T value = (T)createHashMap();
            answer = value;
        }
        else if (Set.class.equals(aClass))
        {
            @SuppressWarnings("unchecked")
            final T value = (T)createHashSet();
            answer = value;
        }

        return answer;
    }

    /**
     * @return a new integer.
     */
    private synchronized Integer createInteger()
    {
        final Integer answer = _integerValue;
        _integerValue += 1;

        return answer;
    }

    /**
     * @return a new long.
     */
    private synchronized Long createLong()
    {
        final Long answer = _longValue;
        _longValue += 1;

        return answer;
    }

    /**
     * @return a new short.
     */
    private synchronized Short createShort()
    {
        final Short answer = _shortValue;
        _shortValue = (short)(_shortValue + 1);

        return answer;
    }

    /**
     * @return a new string.
     */
    private synchronized String createString()
    {
        String answer = null;
        final NumberFormat formatter = NumberFormat.getIntegerInstance();
        formatter.setMinimumIntegerDigits(3);
        formatter.setMaximumIntegerDigits(3);

        answer = "string" + formatter.format(_stringCounter++);

        return answer;
    }

    /**
     * @param <T> Type.
     * @param type Type.
     * 
     * @return the constructor with the fewest arguments, or null if there is
     *         none.
     */
    private <T> Constructor<T> getSimplestConstructor(final Type<T> type)
    {
        Constructor<T> answer = null;

        int minArgs = Integer.MAX_VALUE;

        for (final Constructor<?> constructor : type.getRawClass()
                .getConstructors())
        {
            if (Modifier.isPublic(constructor.getModifiers())
                    && (constructor.getParameterTypes().length < minArgs))
            {
                @SuppressWarnings("unchecked")
                final Constructor<T> value = (Constructor<T>)constructor;
                answer = value;
                minArgs = constructor.getParameterTypes().length;
            }
        }

        return answer;
    }

    /**
     * @param <E> Element type.
     * @param typeParameter Type parameter.
     * 
     * @return a type.
     */
    private <E> Type<E> getType(final TypeParameter<E> typeParameter)
    {
        Type<E> answer = null;

        if (typeParameter instanceof StandardTypeParameter)
        {
            answer = ((StandardTypeParameter<E>)typeParameter).getType();
        }

        return answer;
    }
}
